/**
 * jsCombo - Javascript Dropdown Filtering Library by Kriz Poon, 2010
 * 
 * 
 * FEATURES:
 * ----------------------------------------------------------------------------
 * - Does *NOT* depend on other libraries. Easy to fit into existing pages
 * - No changes needed for the existing drop-down boxes, server side actions etc
 * - Use regular expression to filter options
 * - One object to control multiple drop-down boxes
 * - Easy to setup and use
 * 
 * 
 * INSTRUCTIONS:
 * ----------------------------------------------------------------------------
 * 1. For compatibility of IE 6 and below, an IFRAME object is required to hide
 * dropdown boxes below an absolute layer. Due to technical difficulties to
 * create, insert and set attributes of IFRAME objects in IE, we need to insert
 * the following element inside the body element:
 * 
 * <iframe id="ifrJsComboBackground" src="javascript:'';" style='position:absolute; display:none; width:0px; height:0px' marginwidth="0" marginheight="0" align="bottom" scrolling="no" frameborder="0"></iframe>
 *
 * 
 * 2. Above the existing dropdown box, add a textbox for filtering
 * 
 * <input  type='text'
 *         autocomplete='off'
 *         onkeyup='ComboController.onKeyUp(event, "nameOrIdOFDropdownBox")'
 *         onkeydown='ComboController.onKeyDown(event, "nameOrIdOFDropdownBox")' />
 * 
 * It is important that the browser's default autocomplete behaviour is set to off
 * 
 * 
 * 3. If dropdown boxes do not have an ID or unable to add an ID to it, it is
 * possible to use the name of the dropdown boxes to look up the element.
 * To enable this, invoke the following Javascript during startup:
 * 
 * ComboController.defaultForm = document.nameOfYourForm;
 * 
 * 
 * 4. The style of the auto-complete dropdown box is configurable through CSS.
 * The following are example CSS rules:
 * 
 * <style>
 * .jsComboDropdown 
 * 	  {
 * 	  font-family: Arial, Tahoma, sans-serif;
 * 	  font-size: 12px; line-height: auto; padding:0px;
 * 	  -moz-border-radius: 5px; -webkit-border-radius: 5px;
 * 	  background-color:#daffdb; border:1px solid #32992B;
 * 	  }
 * 
 * .jsComboDropdown a {display:block; margin:3px 0px; text-indent:7px;}
 * .jsComboDropdown a.row            { color:blue; text-decoration:none;}
 * .jsComboDropdown a.row:link       { color:blue; text-decoration:none;}
 * .jsComboDropdown a.row:visited    { color:blue; text-decoration:none;}
 * .jsComboDropdown a.row:active     { color:blue; text-decoration:none;}
 * .jsComboDropdown a.row:hover      { color:blue; text-decoration:none; background-color:white;}
 * 
 * .jsComboDropdown a.selected_row          { color:white; background-color: #32992B; text-decoration:none;}
 * .jsComboDropdown a.selected_row:link     { color:white; text-decoration:none;}
 * .jsComboDropdown a.selected_row:visited  { color:white; text-decoration:none;}
 * .jsComboDropdown a.selected_row:active   { color:white; text-decoration:none;}
 * .jsComboDropdown a.selected_row:hover    { color:white; background-color: #32992B; text-decoration:none; }
 * </style>
 * 
 */

var Log = 
	{
	init: function(div)
		{
		this.div = DomUtil.get(div||'log');
		},
	debug: function(text)
		{
		if (!this.div) return;
		this.div.appendChild(document.createTextNode(text));
		this.div.appendChild(document.createElement('br'));
		},
	clear: function()
		{
		if (!this.div) return;
		while (this.div.lastChild) this.div.removeChild(this.div.lastChild);
		}
	};



var DomUtil = 
	{
	cumulativeOffset: function(element)
		{
		var valueT = 0, valueL = 0;
		do
			{
			valueT += element.offsetTop  || 0;
			valueL += element.offsetLeft || 0;
			element = element.offsetParent;
			} while (element);
		return {'left':valueL, 'top':valueT};
		},
	get: function(idOrElem, form)
		{
		if (!idOrElem) return null;
		if (typeof idOrElem == 'string')
			{
			var ret = document.getElementById(idOrElem);
			if (!ret && !!form)
				{
				// try lookup in the form
				ret = form[idOrElem];
				}
			return ret;
			}
		return idOrElem;
		}
	};



var RegExpUtil = 
	{
	getRegExp: function(query)
		{
		if (!query) return null;
		
		query = query.replace(/\\/g, "\\\\");
		query = query.replace(/\*/g, "\\*");
		query = query.replace(/\./g, "\\.");
		query = query.replace(/\,/g, "\\,");
		query = query.replace(/\!/g, "\\!");
		query = query.replace(/\@/g, "\\@");
		query = query.replace(/\#/g, "\\#");
		query = query.replace(/\$/g, "\\$");
		query = query.replace(/\%/g, "\\%");
		query = query.replace(/\^/g, "\\^");
		query = query.replace(/\&/g, "\\&");
		query = query.replace(/\-/g, "\\-");
		query = query.replace(/\+/g, "\\+");
		query = query.replace(/\=/g, '\\=');
		query = query.replace(/\//g, "\\/");
		query = query.replace(/\;/g, '\\;');
		query = query.replace(/\:/g, '\\:');
		query = query.replace(/\?/g, "\\?");
		query = query.replace(/\'/g, "\\'");
		query = query.replace(/\"/g, '\\"');
		query = query.replace(/\|/g, '\\|');
		query = query.replace(/\</g, "\\<");
		query = query.replace(/\>/g, "\\>");
		query = query.replace(/\(/g, "\\(");
		query = query.replace(/\)/g, "\\)");
		query = query.replace(/\[/g, "\\[");
		query = query.replace(/\]/g, "\\]");
		query = query.replace(/\{/g, "\\{");
		query = query.replace(/\}/g, "\\}");
		query = query.replace(/\~/g, "\\~");
		query = query.replace(/\s+/g, ".*");
	
		return new RegExp(query, "gi");
		}
	};





var DropdownUtil =
	{
	filterOptions: function(options, regExp)
		{
		if (!regExp) return null;
	
		var ret = [];
		try
			{
			for (var i=0; i<options.length; i++)
				{
				var option = options[i];

				if (!option.value) continue; // ignore options with empty value (e.g. "-- Please Select --")
		
				var matches = regExp.exec(option.text);
				
				if (matches)
					{
					ret.push(option);
					}
				}
			}
		catch (e) 
			{
			//Log.debug(e.message);
			}
		return ret;
		}
	};






var ComboController =
	{
	backgroundFrame: null,
	selectDiv: null,
	selectDivClass: 'jsComboDropdown',
	defaultForm: null,

	hideSelect: function()
		{
		if (!this.selectDiv) return;
		this.backgroundFrame.style.display = this.selectDiv.style.display = 'none';
		},
	showSelect: function (filterInput, dataSource)
		{
		filterInput = DomUtil.get(filterInput, this.defaultForm);
		if (!filterInput) return; // error

		var dataSourceId = dataSource;
		dataSource = DomUtil.get(dataSourceId, this.defaultForm);
		if (!dataSource) return; // error
		
		var filterText = filterInput.value;

		var options = DropdownUtil.filterOptions(dataSource.options, RegExpUtil.getRegExp(filterText));
		if (!options || options.length == 0)
			{
			this.hideSelect();
			return;
			}

		// new div
		if (!this.backgroundFrame)
			{
			this.backgroundFrame = DomUtil.get('ifrJsComboBackground');
			}
		if (!this.selectDiv)
			{
			this.selectDiv = document.createElement('DIV');
			this.selectDiv.className = this.selectDivClass;
			this.selectDiv.style.zIndex = 9999;
			this.selectDiv.style.display = 'none';
			document.body.appendChild(this.selectDiv);
			}

		// get filter box offset and dimension
		var pos = DomUtil.cumulativeOffset(dataSource);
		var filterFrame = {width: filterInput.offsetWidth, height: filterInput.offsetHeight};
		var sourceFrame = {width: dataSource.offsetWidth,  height: dataSource.offsetHeight};

		// show selectDiv if hidden
		if (this.selectDiv.style.display == 'none')
			{
			this.backgroundFrame.style.position = this.selectDiv.style.position = 'absolute';
			this.backgroundFrame.style.left = this.selectDiv.style.left = pos.left + 'px';
			this.backgroundFrame.style.top = this.selectDiv.style.top = (pos.top + filterFrame.height) + 'px';
			this.backgroundFrame.style.width = this.selectDiv.style.width = (sourceFrame.width) + 'px';
			this.backgroundFrame.style.height = this.selectDiv.style.height = (200) + 'px';

			this.backgroundFrame.style.display = this.selectDiv.style.display = 'block';
			this.selectDiv.style.overflowY = 'scroll';
			}
		dataSource.filteredOptions = options; // !! appending option elements to div !!

		// clear select div
		while (this.selectDiv.lastChild) this.selectDiv.removeChild(this.selectDiv.lastChild);
		
		// add rows to dropdown
		dataSource.filteredSelectedIndex = -1; // !! using custom attribute !!
		var selectedRow = null;
		for (var i=0; i<options.length; i++)
			{
			var option = options[i];
			if (option.selected) dataSource.filteredSelectedIndex = i; // current selected item
			var row = document.createElement('A');
			row.href = 'javascript:ComboController.onClick("'+dataSourceId+'",'+i+')';
			row.appendChild(document.createTextNode(option.text));
			row.className = (option.selected? ' selected_': '') + 'row';
			this.selectDiv.appendChild(row);

			if (option.selected) selectedRow = row;
			}
		this.adjustScrollPositionForRow(selectedRow, 'MIDDLE');
		},
	adjustScrollPositionForRow: function(row, mode)
		{
		if (!row ||
			!this.selectDiv ||
			this.selectDiv.style.display == 'none') return;

		if (mode == 'MIDDLE')
			{
			this.selectDiv.scrollTop = row.offsetTop - (this.selectDiv.offsetHeight - row.offsetHeight)/2;
			}
		else
			{
			// just ensure visible:
			if (row.offsetTop > this.selectDiv.scrollTop + this.selectDiv.offsetHeight)
				{
				this.selectDiv.scrollTop = row.offsetTop - this.selectDiv.offsetHeight + row.offsetHeight;
				}
			if (row.offsetTop < this.selectDiv.scrollTop)
				{
				this.selectDiv.scrollTop = row.offsetTop;
				}
			}
		},
	changeSelection: function(dataSource, offset)
		{
		dataSource = DomUtil.get(dataSource, this.defaultForm);
		if (!dataSource ||
			!offset ||
			!this.selectDiv ||
			!this.selectDiv.childNodes ||
			!this.selectDiv.childNodes.length ||
			this.selectDiv.style.display == 'none') return;

		var option;
		var fromIdx = dataSource.filteredSelectedIndex;
		var toIdx = dataSource.filteredSelectedIndex+offset;

		if (fromIdx == -1)
			{
			if (offset > 0) toIdx = 0;
			else toIdx = this.selectDiv.childNodes.length - 1;
			}
		
		// change select box's selection
		if (!this.select(dataSource, toIdx)) return;

		// change select div's selection
		var i = 0, n = Math.max(fromIdx, toIdx);
		for (var row = this.selectDiv.firstChild; row; row = row.nextSibling)
			{
			if (i == fromIdx)
				{
				row.className = 'row';
				}
			if (i == toIdx)
				{
				row.className = 'selected_row';

				this.adjustScrollPositionForRow(row);
				}
			if ((i++) >=n) break;
			}
		dataSource.filteredSelectedIndex = toIdx;
		},
	select: function(dataSource, index)
		{
		if (!dataSource) return false;
		
		var option = dataSource.filteredOptions[index];
		if (!option) return false; // error
		
		option.selected = true;
		
		if (typeof dataSource.onchange == 'function')
			{
			dataSource.onchange();
			}
		return true;
		},
	onClick: function(dataSource, index)
		{
		dataSource = DomUtil.get(dataSource, this.defaultForm);
		if (!dataSource ||
			!this.selectDiv ||
			!this.selectDiv.childNodes ||
			!this.selectDiv.childNodes.length ||
			this.selectDiv.style.display == 'none') return;

		this.select(dataSource, index);
		
		this.hideSelect();
		},
	onKeyDown: function(event, dataSource)
		{
		var keycode = event.keyCode;
		if (keycode == 38 || keycode == 40)
			{			
			this.onKey(event, dataSource);
			}
		},
	onKeyUp: function(event, dataSource)
		{
		var keycode = event.keyCode;
		if (keycode == 38 || keycode == 40)
			{
			return;
			}
		
		this.onKey(event, dataSource);
		},
	onKey: function(event, dataSource)
		{
		var target = event.target || event.srcElement;
		if (!target) return; // error

		var keycode = event.keyCode;
		if (event.altKey || event.ctrlKey) return;
		
		if (keycode == 38)
			{
			// select up
			if (this.selectDiv.style.display == 'none')
				{
				this.showSelect(target, dataSource);
				}
			else
				{
				this.changeSelection(dataSource, -1);
				}
			}
		else if (keycode == 40)
			{
			// select down
			if (this.selectDiv.style.display == 'none')
				{
				this.showSelect(target, dataSource);
				}
			else
				{
				this.changeSelection(dataSource, 1);
				}
			}
		else if (keycode == 27 || keycode == 13)
			{
			// escape/enter, show/hide select
			if (this.selectDiv.style.display == 'none')
				{
				this.showSelect(target, dataSource);
				}
			else
				{
				this.hideSelect();
				}
			}
		else
			{
			// filter text
			this.showSelect(target, dataSource);
			}
		}
	};